<?php
/**
 * Groo Book Class.
 *
 * $Horde: incubator/groo/lib/Book.php,v 1.2 2006/08/11 04:39:23 luciano Exp $
 *
 * Copyright 2004-2006 Luciano Ramalho <luciano@ramalho.org>
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
 *
 * @author  Luciano Ramalho <luciano@ramalho.org>
 * @package Groo
 */


error_reporting(E_ALL);
#error_reporting(E_ALL| E_STRICT);
assert_options(ASSERT_ACTIVE,1);
assert_options(ASSERT_WARNING,1); assert_options(ASSERT_BAIL,1);

require_once('ISBN.php');

class BookException extends Exception { };

class Book {
    public $title;
    public $edition;
    public $publisher;
    public $issued;
    public $recordId; // set if book built from a DB record
    protected $creators;
    protected $isbn10; // if set, $isbn13 is calculated and (re)set
    protected $isbn13;    // if set, $isbn10 is calculated and (re)set
    // Note: Bookland EAN is the same as ISBN-13
    
    public function __construct($arg) {
        if (is_string($arg) ) {
            $this->title = $arg;
        } elseif (is_array($arg)) {
            foreach ($this->propNames() as $prop) {
                if (array_key_exists($prop,$arg)) {
                    $this->{$prop} = $arg[$prop];
                }
            }
            if (array_key_exists('isbn',$arg)) {
                $this->setISBN($arg['isbn']);
            }
            if (array_key_exists('isbn13',$arg)) {
                $this->setISBN13($arg['isbn13']);
            }
            if (array_key_exists('ean',$arg)) {
                $this->setISBN13($arg['ean']);
            }
            if (array_key_exists('author',$arg)) {
                $this->addAuthor($arg['author']);
            }
            if (array_key_exists('creators',$arg)) {
                $this->addCreators($arg['creators']);
            }
        }
        if (!isset($this->title) and !isset($this->isbn10) and !isset($this->ean) ) {
            throw new BookException ("One of the fields ['title', 'isbn', 'ean'] must be provided");
        }
    }
    
    public function hasValidISBN() {
        if (isset($this->isbn10)) {
            return ISBN::isValidISBN10($this->isbn10);
        } elseif (isset($this->isbn13)) {
            return ISBN::isValidISBN13($this->isbn13);
        }
        return false;
    }
    
    public function setISBN($digits) {
        $digits = ISBN::filterDigits($digits);
        if (ISBN::isValidISBN10($digits)) {
            $this->isbn10 = $digits;
            $this->isbn13 = ISBN::convertISBN10toISBN13($digits);
        } elseif (ISBN::hasValidISBN10($digits)) {
            $this->isbn10 = ISBN::convertISBN13toISBN10($digits);
            $this->isbn13 = $digits;
        } elseif (ISBN::isValidISBN13($digits)) {
            $this->isbn10 = NULL;
            $this->isbn13 = $digits;
        } else {
            throw new BookException("Invalid ISBN.");    
        }
    }
    
    public function setISBN13($digits) {
        $digits = ISBN::filterDigits($digits);
        if (ISBN::isValidISBN13($digits)) {
            $this->isbn13 = $digits;
            if (ISBN::hasValidISBN10($digits)) {
                $this->isbn10 = ISBN::convertISBN13toISBN10($this->isbn13);
            } else {
                $this->isbn10 = NULL;
            }
        } elseif (ISBN::isValidEAN($digits)) {
            throw new BookException("Valid EAN, but not a valid book EAN");    
        } else {
            throw new BookException("Invalid EAN");    
        }
    }
    
    public function addAuthor($name) {
        $this->creators[] = array('name'=>$name,'role'=>'author');    
    }

    public function addCreators($creators) {
        /*  creators must an array of mappings like this:
            array(array('name'=>'Lewis Carroll','role'=>'author')
                  array('name'=>'Martin Gardner','role'=>'editor'))
        */
        if (is_array($creators) and is_array($creators[0]) and
            array_key_exists('name',$creators[0])) {
            if (isset($this->creators)) { 
                $this->creators = array_merge($this->creators,$creators);
            } else {
                $this->creators = $creators;
            }
        } else {
            throw new BookException ('$creators must be an array of associative arrays');
        }
    }
    
    public function getISBN13() {
        if ($this->isbn13) {
            return $this->isbn13;
        } else {
            return NULL;
        }
    }

    public function getShortestISBN() {
        if ($this->isbn10) {
            return $this->isbn10;
        } else {
            return $this->getISBN13();
        }
    }

    
    public function getTitle() {
        if ($this->title) {
            return $this->title;   
        } else {
            $isbn = $this->getShortestISBN();
            return 'ISBN-'.strlen($isbn).': '.$isbn;    
        }
    }    
    
    public function getSortTitle() {
        if ($this->title) {
            //TODO: implement i18n for this
            //currently these are all English articles and
            //the Portuguese definite articles a, as, o, os
            $articles = array('the','a','an', 'as', 'o', 'os');
            $words = preg_split("/\s+/",$this->title);
            if (in_array(strtolower($words[0]),$articles)) {
                $article = $words[0];
                unset($words[0]);
                return implode(' ',$words);
            } else {
                return $this->title;   
            }
        }
    }
    
    public function getCreatorLinks($searchPage) {
        if ($this->creators) {
            $creators = array();
            foreach ($this->creators as $creator) {
                $searchUrl = Horde::applicationUrl($searchPage.'?author=='.urlencode($creator['name']));
                $creatorStr = '<a href="'.$searchUrl.'">'
                        .$creator['name'] . '</a>';
                if ($creator['role'] != 'author') {
                    $creatorStr .= ' ('.$creator['role'].')'; 
                }
                $creators[] = $creatorStr;
            }
            return implode(', ',$creators);
        } else {
            return '';
        };
    }

    public function propNames() {
        $prop_names = array();
        $class = new ReflectionClass('Book');
        foreach ($class->getProperties() as $prop) {
            if ($prop->isPublic()) {
                $prop_names[] = $prop->getName();
            }
        }
        return $prop_names;
    }
    
    public function toXML() {
        $doc = new DOMDocument('1.0','utf-8');
        $book = $doc->appendChild($doc->createElement('book'));
        foreach ($this->propNames() as $name) {
            if ($this->$name) {
                $value = htmlspecialchars($this->$name,ENT_NOQUOTES,'utf-8');
                $book->appendChild($doc->createElement($name, $value));
            }
        }
        if ($isbn13=$this->getISBN(13)) {
            $book->appendChild($doc->createElement('isbn13', $isbn13));
        }
        foreach ($this->creators as $creator){
            $elem = $doc->createElement('creator', $creator['name']);
            $elem->setAttributeNode(new DOMAttr('role', $creator['role']));
            $book->appendChild($elem);
        }
        return $doc->saveXML();
    }
    
    static function fromXML($xmlstr) {
        $xml = new SimpleXMLElement($xmlstr);
        $book = array();
        $simple_props = array('title','edition','publisher','issued','isbn','isbn13');
        foreach ($simple_props as $prop) {
            $node = $xml->xpath('//'.$prop);
            if ($node) {
                $book[$prop] = (string)$node[0];
            }
        }
        $creators = array();
        foreach ($xml->xpath('//creator') as $node) {
            if ($role = $node['role']) {
                $role = (string)$node['role'];
            } else {
                $role = 'author';
            }
            $name = (string)$node;
            $creators[] = array('name'=>$name, 'role'=>$role);
        }
        $book['creators'] = $creators;
        return new Book($book);
    }

    public function db_insert($db,$source) {
        if (isset($this->isbn13)) {
            if ($id = Book::db_getId($db, 'groo_books', 'isbn13', $this->isbn13)) {
                var_export($this);
                throw new BookException("Book exists (id=$id, isbn13=$this->isbn13)");
            }    
        }    
        $book_id = $db->nextId('groo_books');
        if (PEAR::isError($book_id)) {
            throw new BookException($book_id->getMessage());
        }
        $fields['book_id'] = $book_id;
        foreach ($this->propNames() as $name) {
            $fields[$name] = $db->quoteSmart($this->$name);
        }
        if (isset($this->isbn10)) {
            $fields['isbn'] = $db->quoteSmart($this->isbn10);    
            $fields['isbn13'] = $db->quoteSmart($this->isbn13);    
        }
        if (isset($this->issued) and strlen($this->issued)<10) {
            // publication dates sometimes appear as 2000 or 2000-07
            // so we need to fill the rest to ISO-8601 format
            $fmt = '2000-01-01';
            $iso_date = $this->issued
                .substr($fmt,strlen($this->issued),strlen($fmt)-strlen($this->issued));
            $fields['issued'] = $db->quoteSmart($iso_date);    
        }
        $fields['metadata_source'] = $db->quoteSmart($source);
        $sql = 'INSERT INTO groo_books ('
                    .join(array_keys($fields),',')
                    .') values ('
                    .join(array_values($fields),',') 
                    .')'
                    ;
        $res = $db->query($sql);
        if (PEAR::isError($res)) {
            throw new BookException($res->getMessage());
        }
        $field_names = array('book_id','creator_id','role','citation_order');
        $sqp = $db->autoPrepare('groo_l_book_creator',$field_names, DB_AUTOQUERY_INSERT);
        if (PEAR::isError($res)) {
            throw new BookException($res->getMessage());
        }
        $order = 0;
        foreach ($this->creators as $creator){
            $creator_id = $this->db_produceCreatorId($db, $creator['name'],$source);
            $args = array($book_id, $creator_id, $creator['role'], ++$order);
            $res = $db->execute($sqp, $args);
            if (PEAR::isError($res)) {
                throw new BookException($res->getMessage());
            }
        }
    }
    
    public function db_update($db,$source) {
        $book_id = Book::db_getId($db, 'groo_books', 'isbn13', $this->isbn13);
        if (PEAR::isError($book_id)) {
            throw new BookException($book_id->getMessage());
        }
        foreach ($this->propNames() as $name) {
            if ($this->$name) { //only non-empty fields
                $fields[$name] = $db->quoteSmart($this->$name);
            }
        }
        if (isset($this->issued)) {
            $iso_date = $this->issued;
            if (strlen($iso_date)<10) {
                // publication dates sometimes appear as 2000 or 2000-07
                // so we need to fill the rest to ISO-8601 format
                $fmt = '2000-01-01';
                $iso_date = $iso_date
                    .substr($fmt,strlen($iso_date),strlen($fmt)-strlen($iso_date));
            }
            $fields['issued'] = $db->quoteSmart($iso_date);    
        }
        $fields['metadata_source'] = $db->quoteSmart($source);
        $sql = 'UPDATE groo_books SET';
        $set_clauses = array();
        foreach ($fields as $key=>$value) {
            $set_clauses[] = " $key = $value";
        }
        $sql .= implode(",", $set_clauses);
        $sql .= ' WHERE book_id = '.$book_id;
        $res = $db->query($sql);
        if (PEAR::isError($res)) {
            throw new BookException($res->getMessage());
        }
        $field_names = array('book_id','creator_id','role','citation_order');
        $sqp = $db->autoPrepare('groo_l_book_creator',$field_names, DB_AUTOQUERY_INSERT);
        if (PEAR::isError($res)) {
            throw new BookException($res->getMessage());
        }
        $order = 0;
        foreach ($this->creators as $creator){
            $creator_id = $this->db_produceCreatorId($db, $creator['name'],$source);
            $args = array($book_id, $creator_id, $creator['role'], ++$order);
            $res = $db->execute($sqp, $args);
            if (PEAR::isError($res)) {
                throw new BookException($res->getMessage());
            }
        }
    }

    public function db_produceCreatorId($db,$name,$source) {
        $creator_id = Book::db_getId($db, 'groo_creators','name',$name);
        if (is_null($creator_id)) { // new author, do insert
            $creator_id =$db->nextId('groo_creators');
            $sql =  "INSERT INTO groo_creators"
            ." (creator_id, name, metadata_source, created_dt)"
            ." VALUES (?, ?, ?, NOW())"
            ;
            $cmd = $db->prepare($sql);
            $values = array($creator_id, trim($name));
            // FIXME: metadata_source should be an online source name
            // or a user name (if the record was created manually)
            $values[] = $source;
            $res = $db->execute($cmd,$values);
            if (PEAR::isError($res)) {
                throw new BookException($res->getMessage());
            }
        }
       return $creator_id;
    }
    
    static function db_getId($db, $table_name, $field_name, $field_value) {
        /* this assumes that the first column is the id */
        $sql =  "SELECT * FROM $table_name"
                ." WHERE $field_name = ?"
                ;
        $res = $db->getOne($sql,$field_value);        
        if (PEAR::isError($res)) { throw new BookException($res->getMessage()); }
        if (!is_null($res)) { return (int)$res; }
        return NULL;
    }
    
} // class Book



/*** smoke test ***/
if (isset($argc) and $argc==2 and $argv[1]=='--smoke-test') {
    echo "===============================================\n";
    $alice_xml = '<book source="amazon.com" date="2006-06-19">'
                .'<title>The Annotated Alice: The Definitive Edition</title>'
                .'<isbn13>9780393048476</isbn13>'
                .'<edition>Updated</edition>'
                .'<publisher>W. W. Norton &amp; Company</publisher>'
                .'<issued>1999-11</issued>'
                .'<creator>Lewis Carroll</creator>'
                .'<creator role="editor">Martin Gardner</creator>'
                .'<creator role="illustrator">John Tenniel</creator>'
                .'</book>';
    $alice = Book::fromXML($alice_xml);
    $gof_xml = '<?xml version="1.0" encoding="utf-8"?>'."\n"
                .'<book><title>Design Patterns</title>'
                .'<publisher>Addison Wesley</publisher>'
                .'<isbn13>9780201633610</isbn13>'
                .'<creator role="author">Erich Gamma</creator>'
                .'<creator role="author">Richard Helm</creator>'
                .'<creator role="author">Ralph Johnson</creator>'
                .'<creator role="author">John Vlissides</creator>'
                .'</book>';				
    // $res = Book::fromXML($gof_xml);
    var_dump($alice);
}

?>